/*
 * ENUM Discoverer. ENUM Demonstration software, demonstrating the potential of ENUM.
 * 
 * Copyright (C) 2009 SIDN and ISOC.nl
 * 
 * This file is part of ENUM Discoverer.
 *
 * ENUM Discoverer is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ENUM Discoverer is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with ENUM Discoverer.  If not, see <http://www.gnu.org/licenses/>.
 */

package nl.sidn.enumdiscoverer.ui.incall;

import static android.telephony.TelephonyManager.*;

import static nl.sidn.enumdiscoverer.WebLinkConstants.*;
import static nl.sidn.enumdiscoverer.contacts.ContactProfileAggregator.*;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map.Entry;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;

import nl.sidn.enumdiscoverer.AppConstants;
import nl.sidn.enumdiscoverer.EnumRecord;
import nl.sidn.enumdiscoverer.R;
import nl.sidn.enumdiscoverer.LogConfig;
import nl.sidn.enumdiscoverer.Utils;
import nl.sidn.enumdiscoverer.contacts.ContactDetails;
import nl.sidn.enumdiscoverer.contacts.ContactLock;
import nl.sidn.enumdiscoverer.contacts.ContactProfileAggregator;
import nl.sidn.enumdiscoverer.contacts.dao.ContactsDao;
import nl.sidn.enumdiscoverer.contacts.dao.Coords;
import nl.sidn.enumdiscoverer.contacts.dao.DelayedWritePersonDao;
import nl.sidn.enumdiscoverer.contacts.dao.PersonDao;
import nl.sidn.enumdiscoverer.sync.EnumContactsSync;
import nl.sidn.enumdiscoverer.ui.about.AboutActivity;
import nl.sidn.enumdiscoverer.ui.web.WebActivity;

import android.app.Activity;
import android.app.ActivityGroup;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.location.Address;
import android.location.Geocoder;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.preference.PreferenceManager;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.View.OnClickListener;
import android.widget.ImageView;
import android.widget.TabHost;
import android.widget.TextView;
import android.widget.TabHost.TabSpec;

/**
 * An activity that listens for changes in the state of the current phone call
 * and provides an in-call screen displaying any details that can be found
 * about the other party.
 * Exits soon after the current call ends.
 * 
 * @author	Grant Patterson (grant.patterson@catalyst.net.nz)
 */
public class EnumInCallScreen extends ActivityGroup implements Handler.Callback, OnClickListener {

	static final String TAG = LogConfig.getLogTag(EnumInCallScreen.class);
	// whether DEBUG level logging is enabled (whether globally, or explicitly for this log tag)
	static final boolean DEBUG = LogConfig.isDebug(TAG);
	// whether VERBOSE level logging is enabled
	static final boolean VERBOSE = LogConfig.VERBOSE;
	
	/** A list of EnumRecords for the phone number */
	public static String EXTRA_ENUM_RECORDS = AppConstants.PACKAGE_NAME + ".extra.ENUM_RECORDS";
	
	private static final long WORKER_SHUTDOWN_TIMEOUT = 2000;  // 2 seconds
	
	// set by the build script to indicate whether this build is targeting phones with the Maps API.
	// Maps-specific code is stripped out by the compiler when this is false
	private static final boolean IS_MAPS_BUILD = true;
	
	// Messages passed through mHandler and handled by handleMessage()
	/**
	 * An InputStream containing a photo of the caller/callee
	 */
	private static final int PHOTO_LOADED    = 1;
	/**
	 * Update the UI with contact details
	 */
	private static final int CONTACT_DETAILS = PHOTO_LOADED + 1;
	/**
	 * A HashMap of web links
	 */
	private static final int WEB_LINKS       = CONTACT_DETAILS + 1;
	/**
	 * The android.location.Address resulting from a geocoder lookup
	 */
	static final int GEOCODER_LOOKUP_RESULT = WEB_LINKS + 1;

	private static final int SIP_ICONS = GEOCODER_LOOKUP_RESULT + 1;
	private static final int IM_ICONS = SIP_ICONS + 1;
	
	private static final String LOCATION_TAB_TAG = "location_tab";
	private static final String WEB_TAB_TAG = "web_tab";
	
	
	// instance state bundle keys
	private static final String KEY_NUMBER    = "number";
	private static final String KEY_ENUM_DATA = "enumData";
	// TODO: storing this in the bundle isn't really appropriate. This could change while suspended
	private static final String KEY_PREV_CALL_STATE = "prevCallState";
	// TODO: storing this in the bundle isn't really appropriate. This could change while suspended
	private static final String KEY_CALL_ENDED = "callEnded";
	
	
	private PhoneStateListener mPhoneStateListener = new PhoneStateListener() {
		@Override
		public void onCallStateChanged(int callState, String incomingNumber) {
			EnumInCallScreen.this.onCallStateChanged(callState, incomingNumber);
		}
	};
	
	// A pool of worker threads
	// TODO: look at replacing this with AsyncTasks
	private final ExecutorService mWorkers = Executors.newCachedThreadPool();
	
	// package scope to allow efficient access by inner classes
	final Handler mHandler = new Handler(this);
	
	
	/*
	 * Views
	 */
	private TextView mName;
	private TextView mPhoneNum;
	// current location, if known, otherwise address
	private TextView mLocation;
	private TextView mOrganisation;
	private ImageView mPhoto;
	private ViewGroup mWebLinks;
	private ImageView mPublicKeyIcon;
	private ImageView mSIPIcon;
	private TextView mStatus;
	
	private TabHost mTabs;
	private TabSpec mLocationTab;
	private TabSpec mWebTab;
	
	
	/*
	 * State
	 */
	// The phone number of the call this activity is associated with
	private String mNumber;
	// The ENUM records found for mNumber
	private ArrayList<EnumRecord> mEnumData;
	// the previous call state obtained from TelephonyManager
	private int mPrevCallState = -1;
	// whether the call associated with this instance has ended
	private boolean mCallEnded = false;
	// whether we know the person's current location
	private boolean mLocationKnown = false;
	// whether we have set an image on mPhoto yet
	private boolean mPhotoIsSet = false;
	
	
	@Override
	public boolean handleMessage(Message msg) {
		
		switch (msg.what) {
		case PHOTO_LOADED:
			Bitmap photo = BitmapFactory.decodeStream((InputStream)msg.obj);
			mPhoto.setImageBitmap(photo);
			break;
		case CONTACT_DETAILS:
			updateContactDetails((ContactDetails)msg.obj);
			break;
		case WEB_LINKS:
			setWebLinks((HashMap<Integer, String>)msg.obj);
			break;
		case GEOCODER_LOOKUP_RESULT:
			String location = formatAddressString((Address) msg.obj);
			if (location != null && location.length() > 0) {
				mLocationKnown = true;
				mLocation.setText(location);
			}
			break;
		case NAME_CHANGED:
			mName.setText((String)msg.obj);
			break;
		case ADDRESS_CHANGED:
			// only display if we don't know the current location
			if (!mLocationKnown) {
				mLocation.setText((String)msg.obj);
			}
			break;
		case ORG_CHANGED:
			mOrganisation.setText((String)msg.obj);
			break;
		case PHOTO_CHANGED:
			// to avoid downloading every photo we find, only download if we don't already have a photo
			// TODO: we should update display with the photo that is written to Contacts once ENUM lookup is complete
			if (!mPhotoIsSet) {
				mPhotoIsSet = true;
				String photoUrl = (String)msg.obj;
				runTaskOnWorkerThread(new DownloadPhotoTask(photoUrl));
			}
			break;
		case WEB_LINK_ADDED:
			addWebLink(msg.arg1, (String)msg.obj);
			break;
		case LOCATION_CHANGED:
			updateLocation((Uri)msg.obj, msg.arg1, msg.arg2);
			break;
		case PUBLIC_KEY_SET:
			// display the public key icon
			mPublicKeyIcon.setVisibility(ImageView.VISIBLE);
			break;
		case SIP_ADDRESS_SET:
			// display the SIP icon
			mSIPIcon.setVisibility(ImageView.VISIBLE);
			break;
		case NICKNAME_CHANGED:
			// if no name known, use the nickname
			if (mName.getText() == null || mName.getText().length() == 0) {
				mName.setText((String)msg.obj);
			}
			break;
		case BIRTHDAY_CHANGED:
			break;
		case STATUS_SET:
			mStatus.setText((String)msg.obj);
			break;			
		default:
			// the message hasn't been handled
			return false;
		}
		
		// we have handled the message
		return true;
	}
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		
		if (VERBOSE) Log.v(TAG, "onCreate() called");
		
		// ensure we have default preferences (if none set by user)
		PreferenceManager.setDefaultValues(this, R.xml.preferences, false);
		
		Intent intent = getIntent();
		
		// set up phone state listener
		TelephonyManager tm = (TelephonyManager)getSystemService(TELEPHONY_SERVICE);
		tm.listen(mPhoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
		if (VERBOSE) Log.v(TAG, "Call state on creation: " + callStateToString(tm.getCallState()));
		// We need this to be IDLE initially to get the correct behaviour
		mPrevCallState = TelephonyManager.CALL_STATE_IDLE;
		
		setContentView(R.layout.incall_screen);
		
		mName          = (TextView)  findViewById(R.id.name);
		mPhoneNum      = (TextView)  findViewById(R.id.phoneNumber);
		mLocation      = (TextView)  findViewById(R.id.location);
		mOrganisation  = (TextView)  findViewById(R.id.organisation);
		mPhoto         = (ImageView) findViewById(R.id.photo);
		mWebLinks      = (ViewGroup) findViewById(R.id.link_icon_group);
		mPublicKeyIcon = (ImageView) findViewById(R.id.public_key_icon);
		mSIPIcon 	   = (ImageView) findViewById(R.id.sip_icon);
		mStatus  	   = (TextView)  findViewById(R.id.status);
		
		createTabs();
		
		if (savedInstanceState == null) {
			// we were just launched
			
			mNumber = intent.getStringExtra(Intent.EXTRA_PHONE_NUMBER);
			mPhoneNum.setText(mNumber);
			mEnumData = intent.getParcelableArrayListExtra(EXTRA_ENUM_RECORDS);
			
			startAsyncDetailsLookup();
		
		} else {
			// we are being restored
			if (VERBOSE) Log.v(TAG, "Restored from saved instance state");
			
			mNumber = savedInstanceState.getString(KEY_NUMBER);
			mEnumData = savedInstanceState.getParcelableArrayList(KEY_ENUM_DATA);
			mPrevCallState = savedInstanceState.getInt(KEY_PREV_CALL_STATE);
			mCallEnded = savedInstanceState.getBoolean(KEY_CALL_ENDED);
		}
		
		if (VERBOSE) Log.v(TAG, "created");
	}
	
	private void createTabs() {
		
		// set up tabs
		TabHost tabs = (TabHost)findViewById(R.id.tabhost);
		tabs.setup(getLocalActivityManager());
		TabSpec mainTab = tabs.newTabSpec("main_tab").setIndicator(getString(R.string.tab_contact_title));
		mainTab.setContent(R.id.main_tab);
		tabs.addTab(mainTab);
		tabs.setCurrentTab(0);
		mTabs = tabs;
	}
	
	@Override
	protected void onSaveInstanceState(Bundle outState) {
		super.onSaveInstanceState(outState);
		if (VERBOSE) Log.v(TAG, "Saving instance state");
		
		outState.putString(KEY_NUMBER, mNumber);
		outState.putParcelableArrayList(KEY_ENUM_DATA, mEnumData);
		outState.putInt(KEY_PREV_CALL_STATE, mPrevCallState);
		outState.putBoolean(KEY_CALL_ENDED, mCallEnded);
	}
	
	@Override
	protected void onDestroy() {
		
		if (VERBOSE) Log.v(TAG, "onDestroy() called");
		
		// unregister the listener
		TelephonyManager tm = (TelephonyManager)getSystemService(TELEPHONY_SERVICE);
		tm.listen(mPhoneStateListener, PhoneStateListener.LISTEN_NONE);
		
		// clean up workers
		try {
			boolean timedOut = mWorkers.awaitTermination(WORKER_SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS);
			if (timedOut) {
				Log.w(TAG, "Timed out waiting for ENUM lookup workers to stop");
			}
		} catch (InterruptedException e) {
			Log.w(TAG, "Interrupted before ENUM lookup workers stopped");
		}
		mWorkers.shutdownNow();
		
		super.onDestroy();
		
		if (VERBOSE) Log.v(TAG, "Destroyed");
	}
	
	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		
		MenuInflater inflater = getMenuInflater();
		inflater.inflate(R.menu.incall_screen, menu);
		return true;
	}
	
	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		
		switch (item.getItemId()) {
		case R.id.about:
			// show the 'About' page
			startActivity(new Intent(this, AboutActivity.class));
			return true;
		default:
			return super.onOptionsItemSelected(item);
		}
	}
	
	/*package*/ void onCallStateChanged(int callState, String incomingNumber) {
		
		if (VERBOSE) Log.v(TAG, "Call state change: " + callStateToString(mPrevCallState) +
				" -> " + callStateToString(callState));
		
		// Note: incomingNumber is only set when 
		// CALL_STATE_IDLE -> CALL_STATE_RINGING (starts ringing), or
		// CALL_STATE_RINGING -> CALL_STATE_IDLE (hang up before answering).
		
		// Note: TelephonyManager sometimes notifies us of a call state change to IDLE when
		// first registering a listener, even though the state was already IDLE before registering.
		// To guard against this we obtain the initial call state in onCreate(), and
		// ignore any call state changes that don't differ from the known previous state.
		if (callState == mPrevCallState) {
			// ignore
			if (VERBOSE) Log.v(TAG, "Call state change ignored");
			return;
		}
		
		if (mCallEnded) {
			// we're not interested in any further call state changes
			return;
		}
		
		// update previous call state
		mPrevCallState = callState;
		
		// the call state indicator icon in the banner
		ImageView phoneIcon = (ImageView)findViewById(R.id.phoneicon);
		
		switch (callState) {
		case CALL_STATE_IDLE:
			mCallEnded = true;

			phoneIcon.setBackgroundResource(R.drawable.phoneicon_red);
			break;
		case CALL_STATE_RINGING:
			phoneIcon.setBackgroundResource(R.drawable.phoneicon);
			break;
		case CALL_STATE_OFFHOOK:
			phoneIcon.setBackgroundResource(R.drawable.phoneicon_green);
			break;
		default:
			if (DEBUG) Log.d(TAG, "Unknown call state: " + callState);
			break;
		}
	}
	
	/**
	 * Look up all the data we have for the phone number on another thread, and arrange
	 * for the results to be handled on the UI thread.
	 * This method is intended to be called on the UI thread.
	 */
	private void startAsyncDetailsLookup() {
		
		// Do the lookup in another thread so we don't tie up UI thread
		PersonLookupTask task = new PersonLookupTask(mNumber, mEnumData);
		runTaskOnWorkerThread(task);
	}
	
	/**
	 * Process ENUM Data, updating Contacts if appropriate.
	 * This method shouldn't be called on the UI thread.
	 * precondition: number is not null
	 * @param number The phone number
	 * @param records ENUM records from an ENUM lookup
	 */
	/*package*/ void processEnumData(String number, ArrayList<EnumRecord> records) {
		
		// update contacts
		ContactsDao contactsDao = new ContactsDao(this);
		// TODO: store this somewhere else so we can have a notification that offers to create a contact
		DelayedWritePersonDao personDao = new DelayedWritePersonDao();
		personDao.addPhoneNumber(number);  // avoids bugs when there is no ENUM record containing this number
		ContactProfileAggregator profileAggregator =  new ContactProfileAggregator(personDao, mHandler);
		try {
			EnumContactsSync sync = EnumContactsSync.createWithDefaultMapping(contactsDao);
			sync.syncContact(profileAggregator, records);
			
			// TODO: Don't write to Contacts here. Do it in a notification handler
			// create contact
			PersonDao contact = contactsDao.createContact();
			long id = contact.getId();
			// acquire the lock on contact before writing anything to it
			ContactLock lock = ContactLock.getApplicationWideLock();
			lock.acquire(id);
			try {
				personDao.flushToContact(contact);
			} finally {
				contact.close();
				lock.release(id);
			}
			
		} finally {
			contactsDao.close();
		}
	}
	
	/**
	 * Read all the relevant details from the contact and update the UI (through mHandler)
	 * This method shouldn't be called on the UI thread.
	 * precondition: contact is not null
	 * @param contact A PersonDao to read contact details from
	 */
	/*package*/ void doContactLookup(PersonDao contact) {
		
		Handler handler = mHandler;  // copy to stack
		
		// set various contact details
		Message msg = handler.obtainMessage(CONTACT_DETAILS, contact.getDetails());
		handler.sendMessage(msg);
		
		// set web links
		HashMap<Integer, String> webLinks = contact.getWebLinks();
		if (webLinks != null && webLinks.size() > 0) {
			msg = handler.obtainMessage(WEB_LINKS, contact.getWebLinks());
			handler.sendMessage(msg);
		}
		
		// set photo
		InputStream photo = contact.getPhoto();
		if (photo != null) {
			msg = handler.obtainMessage(PHOTO_LOADED, photo);
			handler.sendMessage(msg);
		}
		
		// set location if known
		Coords location = contact.getLocation();
		if (location != null) {
			msg = handler.obtainMessage(LOCATION_CHANGED, location.getLatitudeE6(),
					location.getLongitudeE6());
			handler.sendMessage(msg);
		}
		
		// set public key
		String pubKey = contact.getPublicKey();
		if (pubKey != null) {
			msg = handler.obtainMessage(PUBLIC_KEY_SET, pubKey);
			handler.sendMessage(msg);
		}
		// set SIP address
		String SIPAddress = contact.getSIPAddress();
		if (SIPAddress != null) {
			msg = handler.obtainMessage(SIP_ADDRESS_SET, SIPAddress);
			handler.sendMessage(msg);
		}
		// set status
		String status = contact.getStatus();
		if (status != null) {
			msg = handler.obtainMessage(STATUS_SET, status);
			handler.sendMessage(msg);
		}
	
	}
	
	private void addWebLink(int service, String url) {
		
		if (VERBOSE) Log.v(TAG, "Processing web link: " + url);
			
		int iconResId;
			
		switch (service) {
		case FACEBOOK:		iconResId = R.drawable.facebook;	break;
		case HYVES:			iconResId = R.drawable.hyves;   	break;
		case TWITTER: 		iconResId = R.drawable.twitter; 	break;
		case LASTFM: 		iconResId = R.drawable.lastfm; 		break;
		case FLICKR: 		iconResId = R.drawable.flickr; 		break;
		case PICASA: 		iconResId = R.drawable.picasa; 		break;
		case YOUTUBE: 		iconResId = R.drawable.youtube; 	break;
		case DELICIOUS: 	iconResId = R.drawable.delicious; 	break;
		case LINKEDIN: 		iconResId = R.drawable.linkedin; 	break;
		case IDENTICA: 		iconResId = R.drawable.identica; 	break;
		case FRIENDFEED:	iconResId = R.drawable.friendfeed; 	break;
		case PLURK: 		iconResId = R.drawable.plurk;	 	break;
		case BRIGHTKITE:	iconResId = R.drawable.brightkite; 	break;
		case TUMBLR: 		iconResId = R.drawable.tumblr; 		break;
		case LIVEJOURNAL:	iconResId = R.drawable.livejournal;	break;
		case DOPPLR: 		iconResId = R.drawable.dopplr;	 	break;
		case MYSPACE: 		iconResId = R.drawable.myspace;	 	break;
		default:
			if (DEBUG) {
				Log.d(TAG, "Unknown web link type: " + service + " for URL " + url);
			}
			return;
		}
		
		// create a link icon for the URL and add to the UI
		createLinkIconView(url, iconResId, mWebLinks);
	}
	
	private void createLinkIconView(String url, int iconResId, ViewGroup parent) {
		
		LayoutInflater inflator = getLayoutInflater();
		// we set attachToRoot to false so that the returned view is the link frame, not parent's root
		View linkFrame =  inflator.inflate(R.layout.weblinktemplate, parent, false);
		ImageView icon = (ImageView)linkFrame.findViewById(R.id.weblink_icon);
		
		icon.setImageResource(iconResId);
		// store the URL in the icon's tag to be retrieved by handler
		icon.setTag(url);
		icon.setOnClickListener(this);
		
		// attach to parent
		parent.addView(linkFrame);
	}
	
	/**
	 * Updates the UI with data from details
	 * @param details Details of the caller/callee to update the UI with
	 */
	private void updateContactDetails(ContactDetails details) {
		
		mName.setText(details.getName());
		mLocation.setText(details.getPostalAddress());
		mOrganisation.setText(details.getOrganisation());
	}
	
	private void setWebLinks(HashMap<Integer, String> links) {
		
		// clear all current web links
		((ViewGroup)findViewById(R.id.link_icon_group)).removeAllViews();
		
		for (Entry<Integer, String> entry : links.entrySet()) {
			addWebLink(entry.getKey(), entry.getValue());
		}
	}
	
	private void updateLocation(Uri uri, final int latitudeE6, final int longitudeE6) {
		
		if (DEBUG) Log.d(TAG, "Updating location to (" + latitudeE6 + ", " + longitudeE6 + ")");
		
		// do a geocoder lookup to turn the coordinates into something more meaningful
		runTaskOnWorkerThread(new GeocodeLookupTask(latitudeE6, longitudeE6));
		
		if (IS_MAPS_BUILD) {
			
			// create an intent describing the location to pass to location tab
			final Intent intent = new Intent();
			intent.setClassName(this, LocationViewerConstants.CLASS_NAME);
			// data ensures any changes to location won't be ignored by Android (extras aren't compared)
			intent.setData(uri);
			intent.putExtra(LocationViewerConstants.EXTRA_LATITUDE, latitudeE6);
			intent.putExtra(LocationViewerConstants.EXTRA_LONGITUDE, longitudeE6);
			
			// if location tab hasn't been initialised, do so now using the intent
			TabSpec locTab = mLocationTab;
			if (locTab == null) {
				locTab = mTabs.newTabSpec(LOCATION_TAB_TAG);
				locTab.setIndicator(getString(R.string.tab_location_title));
				locTab.setContent(intent);
				mTabs.addTab(locTab);
				mLocationTab = locTab;
				
			// otherwise, use the intent to change the location shown
			} else {
				
				if (DEBUG) Log.d(TAG, "Changing location to " + latitudeE6 + "," + longitudeE6);
				
				// set single top flag so that the activity will just be passed the intent,
				// and not restarted
				intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
				
				// if the location tab is currently selected, we need to pass the intent through
				// the LocalActivityManager, since the tab widget won't pick up the change until
				// we move off the tab and back on again.
				if (mTabs.getCurrentTabTag() == LOCATION_TAB_TAG) {
					
					getLocalActivityManager().startActivity(LOCATION_TAB_TAG, intent);
					
				// Otherwise, let the tab widget manage it, so we don't create/update the location
				// activity unnecessarily
				} else {
					
					mLocationTab.setContent(intent);
				}
				
			}
			
			// TODO: an artifact appears between tabs and content after tab is inserted. Find a way to prevent it.
		
		}
	}
	
	private void startWebTab(String url) {
		
		Intent intent = new Intent(this, WebActivity.class);
		intent.setData(Uri.parse(url));
		
		TabSpec webTab = mWebTab;
		if (webTab == null) {
			webTab = mTabs.newTabSpec(WEB_TAB_TAG);
			webTab.setIndicator(getString(R.string.tab_web_title));
			webTab.setContent(intent);
			mTabs.addTab(webTab);
			mWebTab = webTab;
			
		} else {
			
			// assuming the web activity is already running, we can obtain it through
			// the LocalActivityManager - this is what TabHost is using
			Activity activity = getLocalActivityManager().getActivity(WEB_TAB_TAG);
			if (activity == null) {
				Log.w(TAG, "Unable to load URL. Can't find WebActivity");
			}
			WebActivity webActivity =(WebActivity) activity;
			webActivity.setUrlFromIntent(intent);
		}
		
		// assuming this method was called on the user's initiative, the user will expect
		// the web tab to be selected
		mTabs.setCurrentTabByTag(WEB_TAB_TAG);
	}
	
	private String formatAddressString(Address address) {
		String str = "";
		
		int max = address.getMaxAddressLineIndex();
		for (int i = 0; i <= max; i++) {
			str += address.getAddressLine(i) + "\n";
		}
		
		return str;
	}
	
	@Override
	public void onClick(View view) {
		
		if (VERBOSE) Log.v(TAG, "click: id=" + view.getId() + " tag=" + view.getTag());
		
		switch (view.getId()) {
		case R.id.weblink_icon:
			startWebTab((String)view.getTag());
			break;
		default:
			if (DEBUG) Log.d(TAG, "unknown view id: " + view.getId());
			break;
		}
	}
	
	private String callStateToString(int callState) {
		switch (callState) {
		case CALL_STATE_IDLE:
			return "IDLE";
		case CALL_STATE_RINGING:
			return "RINGING";
		case CALL_STATE_OFFHOOK:
			return "OFFHOOK";
		default:
			return "UNKNOWN";
		}
	}
	
	/**
	 * Runs the tasks on one of the available worker threads
	 * @param task
	 */
	private void runTaskOnWorkerThread(Runnable task) {
		
		try {
			mWorkers.execute(task);
		} catch (RejectedExecutionException e) {
			Log.w(TAG, "Task execution rejected for " + task, e);
		}
	}
	
	
	/**
	 * Tries to look up a person's details in Contacts.
	 * If the person can't be found, then the ENUM records are used to
	 * find the person's details
	 */
	private class PersonLookupTask implements Runnable {

		private final String mNumber;
		private final ArrayList<EnumRecord> mEnumData;
		
		PersonLookupTask(String number, ArrayList<EnumRecord> enumData) {
			mNumber = number;
			mEnumData = enumData;
		}
		
		@Override
		public void run() {
			
			String number = mNumber;  // copy to stack
			
			ContactsDao contactsDao = null;
			PersonDao personDao = null;
			
			try {
			
				contactsDao = new ContactsDao(EnumInCallScreen.this);
				personDao = contactsDao.getDaoForContact(number);
				
				if (personDao == null) {
					if (VERBOSE) Log.v(TAG, "No local details found for " + number + ". Using ENUM lookup");
					processEnumData(number, mEnumData);
					return;
				}
				
				// Contact found
				doContactLookup(personDao);
				
			} finally {
				if (contactsDao != null) contactsDao.close();
				if (personDao != null) personDao.close();
			}
		}
	}
	
	/**
	 * Downloads a photo from a URL
	 */
	private class DownloadPhotoTask implements Runnable {
		private final String mPhotoUrl;
		DownloadPhotoTask(String photoUrl) {
			mPhotoUrl = photoUrl;
		}
		@Override
		public void run() {
			InputStream photo = Utils.fetchFromUrl(mPhotoUrl);
			Message msg = mHandler.obtainMessage(PHOTO_LOADED, photo);
			mHandler.sendMessage(msg);
		}
	}
	
	/**
	 * Uses Android's Geocoder to translate the longitude and latitude into an address
	 */
	private class GeocodeLookupTask implements Runnable {
		private final double mLatitude;
		private final double mLongitude;
		GeocodeLookupTask(int latitudeE6, int longitudeE6) {
			mLatitude = latitudeE6 / 1E6;
			mLongitude = longitudeE6 / 1E6;
		}
		@Override
		public void run() {
			Geocoder geocoder = new Geocoder(EnumInCallScreen.this);
			try {
				
				if (VERBOSE) Log.v(TAG, "Looking up address of (" + mLatitude + ", " + mLongitude + ") using Geocoder");
				
				List<Address> addresses = geocoder.getFromLocation(mLatitude, mLongitude, 1);
				
				// addresses should only contain one address (or none). Send it to the UI thread
				if (addresses.size() > 0) {
					
					Address address = addresses.get(0);
					
					if (VERBOSE) Log.v(TAG, "Geocoder found address: " + address);
					
					Message msg = mHandler.obtainMessage(GEOCODER_LOOKUP_RESULT, address);
					mHandler.sendMessage(msg);
				}
				
			} catch (IOException e) {
				Log.i(TAG, "IO error obtaining address from geocoder", e);
			}
		}
	}
}
